<?php
// +----------------------------------------------------------------------
// | INPHP
// | Copyright (c) 2023 https://inphp.cc All rights reserved.
// | Licensed ( https://opensource.org/licenses/MIT )
// | Author: 幺月儿(https://gitee.com/lulanyin) Email: inphp@qq.com
// +----------------------------------------------------------------------
// | 模块对象
// +----------------------------------------------------------------------
namespace Inphp\Core\Object;

use Inphp\Core\Config;
use Inphp\Core\Context;
use Inphp\Core\Service;
use Inphp\Core\Services\Http\Response;
use Inphp\Core\Util\Arr;

class Module
{
    /**
     * 根目录
     * @var string
     */
    public string $root;

    /**
     * 根目录命名空间
     * @var string
     */
    public string $namespace = "";

    /**
     * 模块配置
     * @var array
     */
    public array $config = [];

    /**
     * 文件夹名、模块名
     * @var string
     */
    public string $id = "";

    /**
     * 模块名称
     * @var string
     */
    public string $name = "未知模块";

    /**
     * 版本号
     * @var int
     */
    public int $version = 0;

    /**
     * http配置
     * @var array
     */
    public array $http = [];

    /**
     * ws配置
     * @var array
     */
    public array $websocket = [];

    /**
     * 错误配置
     * @var array
     */
    public array $errors = [];

    /**
     * 模块对象初始化
     * @param string $root      所有模块存放的目录
     * @param string $namespace 所有模块存放的目录空间地址
     * @param string $id   模块文件夹名
     */
    public function __construct(string $root, string $namespace,  string $id)
    {
        $this->root = join("/", [$root, $id]);
        if (is_dir($this->root) && (is_file($this->root."/module.php") || is_file($this->root."/module.json"))) {
            //引入配置
            $config = is_file($this->root."/module.php") ? (include $this->root."/module.php") : file_get_contents($this->root."/module.json");
            $this->config = is_array($config) ? $config : (@json_decode($config, true) ?? []);
            //读取配置
            $this->id = $id;
            $this->name = $this->config["name"];
            $this->version = $this->config["version"];
            $this->http = $this->config["http"] ?? [];
            $this->websocket = $this->config["websocket"] ?? [];
            //自动补全该属性，强制使用控制器处理
            $this->websocket["controller"] = true;
            $this->namespace = $namespace."\\".$id;
            $this->namespace = str_replace("\\\\", "\\", $this->namespace);
            //自动加载
            if (!empty($this->config["autoload"])) {
                foreach ($this->config["autoload"] as $file) {
                    $file = $this->root."/".$file;
                    if (is_file($file)) {
                        require_once $file;
                    }
                }
            }
        }
    }

    /**
     * 获取内部模块列表
     * @param string $type
     * @return array
     */
    public function getModules(string $type = Service::HTTP): array
    {
        return $this->{$type}["modules"] ?? [];
    }

    /**
     * 仅获取HTTP的模块列表
     * @return array
     */
    public function getHttpModules(): array
    {
        return $this->getModules();
    }

    /**
     * 仅获取websocket的模块列表
     * @return array
     */
    public function getWebsocketModules(): array
    {
        return $this->getModules(Service::WEBSOCKET);
    }

    public function getDefaultModule(string $type = Service::HTTP): string
    {
        //配置
        $config = $this->{$type};
        //默认模块
        $defaultModuleName = $config["defaultModule"] ?? null;
        //先获取模块列表
        $moduleList = $this->getModules($type);
        if (empty($moduleList)) {
            //如果没有模块，直接返回空字符即可
            return "";
        }
        //如果未设置或为空，使用第一个
        $defaultModuleName = !empty($defaultModuleName) ? $defaultModuleName : reset($moduleList);
        //如果值不正确，自动修正
        return in_array($defaultModuleName, array_values($moduleList)) ? $defaultModuleName : reset($moduleList);
    }

    /**
     * 根据内部模块名称，获取uri入口
     * @param string $name
     * @param string $type
     * @return string
     */
    public function getModuleUri(string $name, string $type = Service::HTTP): string
    {
        //先获取模块列表
        $moduleList = $this->getModules($type);
        if (empty($moduleList)) {
            //并不存在内部模块列表，原值返回即可
            return $name;
        }
        //反转键值
        $moduleList = array_flip($moduleList);
        //如果存在，直接返回，如果不存在，加上默认模块前缀，以自动修正
        return $moduleList[$name] ?? (join("/", [reset($moduleList), $name]));
    }

    /**
     * 根据地址，获取内部模块名称
     * @param string $uri
     * @param string $type
     * @return string
     */
    public function getModuleNameByUri(string $uri, string $type = Service::HTTP): string
    {
        //先获取模块列表
        $moduleList = $this->getModules($type);
        return !empty($moduleList) && isset($moduleList[$uri]) ? $moduleList[$uri] : "";
    }

    /**
     * 获取内部模块的响应流格式，应用于header的Content-Type值
     * @param string|null $name 模块名称
     * @return string           返回Content-Type的值，默认为 text/html
     */
    public function getModuleHttpResponseContentType(?string $name = null): string
    {
        $responseList = $this->http["response"] ?? [];
        $responseList = is_array($responseList) ? $responseList : ["contentType" => $responseList];
        $config = !is_null($name) ? ($responseList[$name] ?? $responseList) : $responseList;
        $type = $config["contentType"] ?? Response::CONTENT_TYPE_HTML;
        return !empty($type) && in_array($type, Response::CONTENT_TYPES) ? $type : Response::CONTENT_TYPE_HTML;
    }

    /**
     * 获取服务所在的命名空间
     * @param string $type
     * @return string
     */
    public function getServiceHomeNamespace(string $type = Service::HTTP): string
    {
        return join("\\", [$this->namespace, $this->{$type}["root"] ?? ""]);
    }

    /**
     * 处理请求并获取到路由状态
     * @param string $uri               请示路径：子模块入口/控制器/方法
     * @param string $requestMethod
     * @param string $type
     * @return RouterStatus
     */
    public function matchRouterStatus(string $uri = "", string $requestMethod = "GET", string $type = Service::HTTP): RouterStatus
    {
        if (empty($this->{$type})) {
            return new RouterStatus([
                "status"    => 404,
                "message"   => "未配置的请求",
                "method"    => $requestMethod
            ]);
        }
        //请求路径处理
        $pathMap = [];
        $path = explode("/", $uri);
        foreach ($path as $p) {
            if (!empty($p)) {
                $p = strrchr($p, ".php") === ".php" ? substr($p, 0, -4) : $p;
                $pathMap[] = $p;
            }
        }
        //所有模块
        $moduleList = $this->getModules($type);
        //默认模块
        $defaultModuleName = $this->getDefaultModule($type);
        //默认模块Uri
        $defaultModuleUri = $this->getModuleUri($defaultModuleName, $type);
        //需要给路径补充满
        if (empty($moduleList)) {
            //模块列表为空，说明目录入就是控制器，所以至少满2个值：控制器/方法
            $pathMap = array_pad($pathMap, 2, "index");
        } else {
            //如果为空，填充默认模块
            $pathMap = empty($pathMap) ? [$defaultModuleUri] : $pathMap;
            //处理入口
            $moduleName = $this->getModuleNameByUri($pathMap[0], $type);
            //若没取到，自动修正
            if (empty($moduleName)) {
                $pathMap = array_merge([$defaultModuleUri], $pathMap);
                $moduleName = $defaultModuleName;
            }
            //非空模块，至少填充满3个值：子模块入口/控制器/方法
            $pathMap = array_pad($pathMap, 3, "index");
        }
        //先过滤

        //控制器的命名空间前缀
        $prefix = $this->getServiceHomeNamespace($type);
        //控制器
        $controller = $urlPath = null;
        //方法
        $method = null;
        //已尝试的控制器
        $controllers = [];
        //是否使用控制器，支持单独配置
        $useController = $this->{$type}["controller"];
        //2023.11.8 支持配置默认控制器
        $defaultController = null;
        if (is_array($useController) && isset($moduleName)) {
            $val = $useController[$moduleName] ?? false;
            if (is_array($val) || is_string($val)) {
                $useController = true;
                $defaultController = $moduleName."\\".(is_string($val) ? $val : ($val["default"] ?? "index"));
            } else {
                $useController = $val;
            }
        }
        //处理，查找有没有对应的控制器视图文件
        if ($type !== Service::HTTP || $useController) {
            //非http请求 或 指明开启了控制器
            //先匹配完整的 控制器/方法名，即pathMap的最后一个值是方法名
            $controller = $urlPath = join("\\", array_slice($pathMap, 0, -1));
            $controllers[] = $prefix."\\".$controller;
            $method = end($pathMap);
            if (!class_exists($prefix."\\".$controller)) {
                //不存在，尝试补充方法名为 index，即，pathMap
                $controller = $urlPath = join("\\", $pathMap);
                $controllers[] = $prefix."\\".$controller;
                $method = "index";
            }
            $method = class_exists($prefix."\\".$controller) ? $method : "index";
            $controller = class_exists($prefix."\\".$controller) ? $controller : $defaultController;
        }
        //路由状态对象
        $status = new RouterStatus([
            //路径
            "path"          => str_replace("\\", "/", $prefix."\\".$urlPath),
            //控制器
            "controller"    => $controller ? [($prefix."\\".$controller), $method] : null,
            //方法
            "method"        => $requestMethod,
            //模块
            "module"        => $this->id,
            //默认状态200
            "status"        => 200
        ]);

        //如果是HTTP，可能还需要视图文件
        if ($type == Service::HTTP) {
            //获取响应流类型
            $status->responseContentType = $this->getModuleHttpResponseContentType($moduleName ?? null);
            //如果响应流是 text/html，需要配合视图处理
            if ($status->responseContentType === Response::CONTENT_TYPE_HTML) {
                //视图文件夹：{moduleRoot}/{view}/子模块名称
                $viewDir = join("/", [$this->root, $this->http["view"]]).(isset($moduleName) ? "/{$moduleName}" : "");
                if (is_dir($viewDir)) {
                    //视图文件，仅支持HTML文件，需要去除 moduleName
                    $pathMap = isset($moduleName) ? array_slice($pathMap, 1) : $pathMap;
                    $dirMap = join("/", array_slice($pathMap, 0, -1));
                    $file = strrchr($dirMap, ".html") === ".html" ? $dirMap : ($dirMap.".html");
                    $files = [substr($viewDir."/".$file, strlen($this->root))];
                    if (!is_file($viewDir."/".$file)) {
                        //再尝试
                        $dirMap = join("/", $pathMap);
                        $file = strrchr($dirMap, ".html") === ".html" ? $dirMap : ($dirMap.".html");
                        $files[] = substr($viewDir."/".$file, strlen($this->root));
                        if (!is_file($viewDir."/".$file)) {
                            $files = join(", ", $files);
                            $status->status = !$useController ? 404 : 200;
                            $status->message = "模块[{$this->id}], 未找到视图文件：[$files]";
                        }
                    }
                    $status->state = "view";
                    $status->viewDir = $status->status == 200 ? $viewDir : null;
                    $status->view = $status->status == 200 ? $file : null;
                } else {
                    $status->state = "view";
                    $status->status = !$useController ? 404 : 200;
                    $status->message = "模块[{$this->id}], 未找到视图文件夹：[".substr($viewDir, strlen($this->root))."]";
                }
            }
        } else {
            $status->responseContentType = "websocket message";
        }
        if ((is_null($status->controller) || !class_exists($status->controller[0])) && ($status->status != 200 || $useController)) {
            //
            $controllers = join(", ", $controllers);
            //如果不需要控制器
            $status->status = 404;
            if ($useController) {
                $status->message = "未找到控制器：[{$controllers}]";
            }
            //标记为空
            $status->controller = null;
            //$status->method = null;
            //如果是404
            if ($status->state == "controller") {
                $status->viewDir = null;
                $status->view = null;
            }
        }
        //请求header、请求方式、请求来源(系统绑定的域名会在白名单中)处理...
        if ($status->status === 200) {
            //此处的过滤，仅能通过模块配置的参数来配合处理，控制器想关的注解(类似@Method、@Access)都不会在此实现
            $responseList = $this->config[$type]["response"] ?? [];
            $responseList = is_array($responseList) ? $responseList : [];
            $responseConfig = isset($moduleName) ? ($responseList[$moduleName] ?? $responseList) : $responseList;
            $accessHeaders = $responseConfig["accessHeaders"] ?? "";
            if (!empty($accessHeaders)) {
                $status->setAccessControlAllowHeaders($accessHeaders);
            }
            $accessMethods = $responseConfig["accessMethods"] ?? "*";
            if (!empty($accessMethods)) {
                $status->setAccessControlAllowMethods($accessMethods);
            }
            $accessOrigins = $responseConfig["accessOrigins"] ?? "";
            if (!empty($accessOrigins)) {
                $status->setAccessControlAllowOrigins($accessOrigins);
            }
            //预检
            $client = Context::getClient();
            if (!$status->checkHeaders($client->header)) {
                $status->status = 403;
                $status->message = "预检header未通过，该请求被拒绝";
            } elseif (!$status->checkMethod()) {
                $status->status = 403;
                $status->message = "预检请求方法未通过，该请求被拒绝";
            } elseif (!$status->checkOrigin($client->origin)) {
                $status->status = 403;
                $status->message = "预检来源未通过，该请求被拒绝";
            }
        }
        //返回路由状态
        return $status;
    }

    /**
     * 匹配并获取完整的url
     * @param string $path
     * @return string
     */
    public function matchUri(string $path): string
    {
        if (empty($path)) {
            return "";
        }
        $pathArr = explode("/", $path);
        $url = $this->getModuleUri($pathArr[0]);
        return join("/", [$url, join("/", array_slice($pathArr, 1))]);
    }

    /**
     * 获取业务配置（非模块配置）
     * 会优先使用自定义配置
     * @param string|null $map
     * @param string $name
     * @return mixed
     */
    public function getConfig(?string $map = null, string $name = "config"): mixed
    {
        if (is_null($name)) {
            //默认获取模块配置
            return Arr::get($this->config, $map);
        }
        //默认配置
        $config = [];
        $file = $this->root."/{$name}.php";
        if (is_file($file)) {
            $config = include $file;
            $config = is_array($config) ? $config : [];
        }
        //默认读取自定义配置
        $customerConfig = Config::get("public.{$this->id}.{$name}");
        if (!empty($customerConfig)) {
            //优先读
            return Arr::get($customerConfig, $map, Arr::get($config, $map));
        }
        return Arr::get($config, $map);
    }

    /**
     * 保存配置
     * @param string $name
     * @param string|array $content
     * @return bool
     */
    public function saveConfig(string $name, string|array $content): bool
    {
        //2023.8.24 配置文件使用JSON保存
        $file = "public/{$this->id}/{$name}.json";
        return Config::save($file, $content);
    }

    /**
     * 获取控制台配置的菜单列表
     * @return array
     */
    public function getAdminMenus(): array
    {
        $menus = $this->getConfig("menus", "admin");
        $menus = is_array($menus) ? $menus : [];
        return !empty($menus) ? $this->parseMenuUrl($menus) : [];
    }

    /**
     * 转化菜单URL
     * @param array $menus
     * @return array
     */
    public function parseMenuUrl(array $menus): array
    {
        $list = [];
        foreach ($menus as $menu) {
            if (isset($menu["url"]) && !empty($menu["url"]) && stripos($menu["url"], "http") !== 0) {
                if (stripos($menu["url"], ".") === 0) {
                    $menu["url"] = url(substr($menu["url"], 1), $this->id);
                } elseif (stripos($menu["url"], "/") === 0) {
                    $menu["url"] = url($menu["url"]);
                }
            }
            if (isset($menu["subMenu"]) && !empty($menu["subMenu"])) {
                $menu["subMenu"] = $this->parseMenuUrl($menu["subMenu"]);
            }
            $list[] = $menu;
        }
        return $list;
    }

    /**
     * 获取错误
     * @param int|null $code
     * @return string|array
     */
    public function getError(?int $code): string|array
    {
        if (empty($this->errors)) {
            if (is_file($this->root."/error.php")) {
                $this->errors = include $this->root."/error.php";
            }
        }
        $this->errors = is_array($this->errors) ? $this->errors : [];
        $error = $this->errors[$code] ?? "unknown error code：{$this->id}/{$code}";
        if (is_array($error)) {
            //多国语言配置
            $client = getClient();
            return $error[$client->lang] ?? reset($error);
        }
        return $error;
    }
}